package com.czl.myRabbitMq.mqServer.dataCenter;


import com.czl.myRabbitMq.commom.BinaryTransformTool;
import com.czl.myRabbitMq.commom.exception.FileFormatException;
import com.czl.myRabbitMq.commom.exception.FileNotExitException;
import com.czl.myRabbitMq.commom.exception.mqException;
import com.czl.myRabbitMq.mqServer.core.MSGQueue;
import com.czl.myRabbitMq.mqServer.core.Message;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.LinkedList;
import java.util.Scanner;

/**
 * 我们规定每个队列下都有一个和队列名字相同的目录来存储对印队列的消息
 * 同时每个目录下有两个文件 一个是  queue_data.txt 这个存储消息的内容
 * 另外一个文件是  queue_stat.txt这个文件就两个参数一个是 totalCount
 * 表示总的消息数量 另一个是validCount表示有效消息数量.
 */
@Slf4j
public class MessageFileManager {

    public static class Stat {
        public int totalCount; //总的消息数目
        public int validCount; //有效消息数目
    }
    //这里并不做任何操作 为了方便后续扩展和DiskDataManager类格式统一
    public void init(){

    }

    //获取到当前交换机消息所在目录 (和交换ExchangeName同名)
    public String getQueueFileName(String exchangeName) {
        return "./data/" + exchangeName;
    }

    //获取到交换机消息目录下的queue_data.txt文件
    public String getQueueDataFileName(String exchangeName) {
        return getQueueFileName(exchangeName) + "/queue_data.txt";
    }

    //获取到交换机消息目录下的queue_stat.txt文件
    public String getQueueStatFileName(String exchangeName) {
        return getQueueFileName(exchangeName) + "/queue_stat.txt";
    }

    //用垃圾回收机制回收垃圾的时候 使用该方法获取复制文件的路径
    public String getQueueDataNewFileName(String exchangeName) {
        return getQueueFileName(exchangeName) + "/queue_new_data.txt";
    }

    //读取交换机下的queue_stat文件
    @SneakyThrows
    private Stat readStat(String queueName) {
        //因为queue_stat中 文件是txt格式所以可以直接使用scanner进行读取
        Stat stat = new Stat();
        try (InputStream inputStream = Files.newInputStream(Paths.get(getQueueStatFileName(queueName)))) {
            Scanner scanner = new Scanner(inputStream);
            //用scanner读取的时候 nextInt表示一次遇到的int类型的数字
            stat.totalCount = scanner.nextInt();
            stat.validCount = scanner.nextInt();
        }
        return stat;
    }

    //写入queue_stat文件
    @SneakyThrows
    private void writeStat(String queueName, Stat stat) {
        //每次重新写 即可 因为默认写数据会清楚掉文件原有的内容
        try (OutputStream outputStream = Files.newOutputStream(Paths.get(getQueueStatFileName(queueName)))) {
            PrintWriter printWriter = new PrintWriter(outputStream);
            printWriter.write(stat.totalCount + "\t" + stat.validCount);
            printWriter.flush();
        }
    }

    //创建出队列文件和队列文件下对应的文件
    @SneakyThrows
    public void createQueueDefaultFile(String queueName) {
        //创建默认的队列文件
        File queueFile = new File(getQueueFileName(queueName));
        if (!queueFile.exists()) {//文件不存在时才进行创建
            log.info("默认文件创建成功");
            if (!queueFile.mkdirs()) //创建文件失败后 抛出异常
                throw new IOException("创建文件失败" + getQueueFileName(queueName));
        }


        //创建默认的消息内容文件
        File dataFile = new File(getQueueDataFileName(queueName));
        if (!dataFile.exists()) {
            log.info("创建默认消息内容文件成功");
            if (!dataFile.createNewFile())
                throw new IOException("创建默认消息内容文件失败" + getQueueDataFileName(queueName));
        }


        //创建默认的stat文件
        File statFile = new File(getQueueStatFileName(queueName));
        if (!statFile.exists()) {
            log.info("创建stat文件成功");
            if (!statFile.createNewFile())
                throw new IOException("创建stat文件失败" + getQueueStatFileName(queueName));
        }


        //在默认文件创建完成之后 给stat插入默认的数据 0/t0 表示总消息0条 有效消息0条
        try (OutputStream outputStream = Files.newOutputStream(Paths.get(getQueueStatFileName(queueName)))) {
            PrintWriter printWriter = new PrintWriter(outputStream);
            printWriter.write(0 + "\t" + 0);
            printWriter.flush();
        }
    }

    //队列目录和文件的删除
    @SneakyThrows
    public void deleteQueueFile(String queueName) {
        File statFile = new File(getQueueStatFileName(queueName));
        if (statFile.exists()) {

            if (!statFile.delete())
                throw new IOException("文件删除失败" + getQueueStatFileName(queueName));

            log.info("删除stat成功");
        }
        File dataFile = new File(getQueueDataFileName(queueName));
        if (dataFile.exists()) {

            if (!dataFile.delete())
                throw new IOException("删除失败" + getQueueDataFileName(queueName));

            log.info("删除data成功");
        }
        File queueFile = new File(getQueueFileName(queueName));
        if (queueFile.exists()) {

            if (!queueFile.delete())
                throw new IOException(getQueueFileName(queueName));

            log.info("删除queue成功");
        }
    }

    //检查队列目录下是否存在 queue_data和queue_stat对象
    private boolean checkQueueFileExit(String queueName) {
        File dataFile = new File(getQueueDataFileName(queueName));
        File statFile = new File(getQueueStatFileName(queueName));

        if (dataFile.exists() && statFile.exists())
            return true;

        return false;
    }

    /**
     * 在文件中写入消息数据
     *
     * @param queue   那条交换机下的数据
     * @param message 写入的消息
     */
    @SneakyThrows
    public void writeMessageData(MSGQueue queue, Message message) {
        //在写入消息前 我们应检查是否存在相应的文件和目录
        if (!checkQueueFileExit(queue.getName()))
            throw new FileNotExitException("队列文件不存在" + queue.getName());
        //写入消息前我们应该将消息序列化
        byte[] messageBytes = BinaryTransformTool.getBinaryData(message);

        synchronized (queue) {
            //这里我们打开queue_data这个文件 同时第二个参数修改为true 不加第二个参数不是追加 每一次写会默认覆盖上一次内容
            try (OutputStream outputStream = new FileOutputStream(getQueueDataFileName(queue.getName()), true)) {
                //我们在写入消息前需要记录消息中的 offsetBegan和offsetEnd属性 这样我们可以在后续中直接找到消息的位置
                //同时我们规定在文件中，消息的存储形式为，前4个字节存储消息内容的大小 所以假设第一条消息大小为20 第二题消息大小
                //为10 第一个消息的offsetBegan应该为0+4 = 4 然后他的offsetEnd为 4+20 = 24 第二条消息offsetBegan为 24+4 = 28
                //每条消息的前4个字节就存储这个消息的大小 文件开始的4个字节就存储 20
                File dataFile = new File(getQueueDataFileName(queue.getName()));
                message.setOffsetBegan(dataFile.length() + 4);
                message.setOffsetEnd(dataFile.length() + 4 + messageBytes.length);
                //设置完偏移量属性 首先需要写入消息前4个字节(消息整体的大小)
                //在写入消息前4个字节我们可以使用位运算符来解决  但是java标准库中封装了相应的方法
                try (DataOutputStream dataOutputStream = new DataOutputStream(outputStream)) {
                    dataOutputStream.writeInt(messageBytes.length);
                    //写入完消息长度后开始写入消息
                    dataOutputStream.write(messageBytes);
                }
                //消息写入完之后需要更新queue_stat文件的内容!!!!
                Stat stat = readStat(queue.getName());
                stat.validCount += 1;
                stat.totalCount += 1;
                writeStat(queue.getName(), stat);
            }
        }
    }

    /**
     * 删除文件中的消息内容
     * 1.找到文件中的消息内容
     * 2.将文件反序列化同时将文件中的 isDelete 属性改为0x0
     * 3.将文件重新序列化后写入原来的位置
     *
     * @param queue
     * @param message 要删除的消息是那一条
     */
    //这里为了找到消息在文件中的位置 我们使用RandomAccessFile这个类
    //这个类支持随机访问(也就类似于访问数组下标元素类似数组也是因为随机访问所以才能达到O(1)的水平)的方式 普通的OI对象不支持随机访问
    @SneakyThrows
    public void deleteMessageData(MSGQueue queue, Message message) {
        //计算出文件内消息大小
        int length = (int) (message.getOffsetEnd() - message.getOffsetBegan());
        byte[] bytes = new byte[length];
        try (RandomAccessFile randomAccessFile = new RandomAccessFile(getQueueDataFileName(queue.getName()), "rw")) {
            //RandomAccessFile类中的seek方法是改变文件读取光标的 也就是随机访问的意思
            //这里是改变光标到读取所在消息的开头
            randomAccessFile.seek(message.getOffsetBegan());
            //传入字节数组 会自动从光标位置读满字节数组 但需要注意的是 读取过程中光标也会发生改变
            //后面需要重新设置光标位置
            randomAccessFile.read(bytes);
            //将消息对象反序列化回原来对象
            Message disMessage = (Message) BinaryTransformTool.fromData(bytes);
            //将逻辑删除标志位设置为真
            disMessage.setIsDelete((byte) 0x0);
            //重新将逻辑删除后的消息序列化后重新写入文件中
            bytes = BinaryTransformTool.getBinaryData(disMessage);
            randomAccessFile.seek(message.getOffsetBegan());
            randomAccessFile.write(bytes);
            //???????????????????????????????????????????????
        }
        //修改完之后需要修改queue_stat文件中的数据!!!!!!!!!!!!!
        Stat stat = readStat(queue.getName());
        if (stat.validCount > 0)
            stat.validCount -= 1;
        writeStat(queue.getName(), stat);
    }

    /**
     * 该方法在程序执行时启动 作用为将消息文件内容加载进内存中
     */
    @SneakyThrows
    public LinkedList<Message> loadMessage(String queueName) {
        LinkedList<Message> messageList = new LinkedList<>();
        //先找到消息文件所在的位置 按照消息存储的规则进行消息的读取
        try (InputStream inputStream =
                     Files.newInputStream(Paths.get(getQueueDataFileName(queueName)))) {
            //因为每个消息的offsetBegan和offsetEnd方法没有被序列化进文件中
            //这个变量用来记录 每一次对象的offsetBegan和offsetEnd属性
            int currentSeek = 0;
            //因为这里需要每次固定读取4个字节 我们可以使用位运算 但是为了方便 使用java封装好的类DataInputStream
            try (DataInputStream dataInputStream = new DataInputStream(inputStream)) {
                while (true) {
                    //读取一个消息的方法 读取的fileLength为后面消息体的长度
                    //同时这里的方法可能会读到文件的末尾
                    //Throws:
                    //EOFException – if this input stream reaches the end before reading four bytes.
                    //官方方法写的注释
                    int fileLength = dataInputStream.readInt();
                    byte[] bytes = new byte[fileLength];
                    int realLength = dataInputStream.read(bytes);
                    //要是前面表示消息的4个字节读取出来的数字 和 实际读取的消息大小不符合
                    //说明当前消息的格式已经错误了
                    if (realLength != fileLength)
                        throw new FileFormatException("消息文件格式错误" + getQueueFileName(queueName));
                    //读取出一个消息之后 将消息反序列化 要是消息中的isDelete为0x0则表示是无效消息 就不加载进入内存
                    Message message = (Message) BinaryTransformTool.fromData(bytes);

                    message.setOffsetBegan(currentSeek + 4);
                    message.setOffsetEnd(currentSeek + 4 + fileLength);
                    //设置完毕之后更新指向
                    currentSeek += (4 + fileLength);

                    if (message.getIsDelete() == 0x1)
                        messageList.add(message);
                }
                //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
                //这里的catch并非是处理异常的 而是处理正常的逻辑的因为上述方法dataInputStream.readInt();
                //中读取到文件末尾后, 会抛出一个 EOFException异常 所以这里的catch仅仅用于处理正常结束的逻辑
            } catch (EOFException e) {
                log.info("文件加载完毕");
            }
            return messageList;
        }
    }

    /**
     * 这里我们的判断条件为 总消息条数达到1000 且 无效消息占其中百分40以上 则触发垃圾回收机制
     * 同时我们使用复制算法 将有效的消息全部复制到一个新的queue_data文件
     */
    public boolean checkGC(String queueName) {
        Stat stat = readStat(queueName);
        if (stat.totalCount > 1000 && (double) stat.validCount / (double) stat.totalCount < 0.6)
            return true;
        return false;
    }

    /**
     * 本方式实现真正的垃圾回收 使用复制算法 将一个文件有效消息复制到另一个空文件中
     * 复制完毕之后 ，将旧文件删除 将新文件改名为旧文件
     * 同时因为垃圾回收的过程可能会非常占用时间 所以这里我们使用一把锁 防止在垃圾回收的过程中
     * 其他线程又一边写入消息导致系统混乱
     */

    @SneakyThrows
    public void gc(MSGQueue queue) {
        synchronized (queue) {
            //垃圾回收可能会消耗很长的时间 这里做一个记录
            long began = System.currentTimeMillis();
            //这里创建一个新文件
            File dataNewFile = new File(getQueueDataNewFileName(queue.getName()));
            //dataNewFile此处应该是不存在的 要是存在 则前面某个步骤应该发生了异常
            if (dataNewFile.exists())
                throw new mqException("垃圾回收过程存在异常 new_data文件未删除" +
                        getQueueDataFileName(queue.getName()));

            if(!dataNewFile.createNewFile())
                throw new mqException("垃圾回收临时文件创建失败");

            //获取到消息中的有效消息  上述已经实现该方法
            LinkedList<Message> messageList = loadMessage(queue.getName());

            try(OutputStream outputStream =
                        new FileOutputStream(getQueueDataNewFileName(queue.getName()),true)) {

                try(DataOutputStream dataOutputStream =
                            new DataOutputStream(outputStream)){

                    for(Message message : messageList){

                        byte[] bytes = BinaryTransformTool.getBinaryData(message);
                        dataOutputStream.writeInt(bytes.length);
                        dataOutputStream.write(bytes);
                    }
                }
            }
            //拷贝完之后需要将文件命名为原来的文件
            File oldFile = new File(getQueueDataFileName(queue.getName()));

            if(oldFile.delete())
                log.info("原文件删除成功");

            if(!dataNewFile.renameTo(oldFile))
                throw new mqException("文件改名失败");

            //因为垃圾回收之后全是有效消息了
            Stat stat = readStat(queue.getName());
            stat.totalCount = messageList.size();
            stat.validCount = messageList.size();
            writeStat(queue.getName(),stat);

            long end = System.currentTimeMillis();
            log.info("本次垃圾回收一共耗时#{}",began - end);
        }
    }
}

